<template>
  <div ref="statsRef"></div>
  <div ref="containerRef" class="container">
  </div>
</template>

<script lang="ts" setup>

import {
  ACESFilmicToneMapping, AxesHelper,
  Color,
  EquirectangularReflectionMapping, Fog, GridHelper, Material, Mesh, MeshPhysicalMaterial, Object3D,
  PerspectiveCamera,
  Scene,
  sRGBEncoding,
  WebGLRenderer
} from "three";
import Stats from "stats.js"
import * as dat from "dat.gui"
import {onMounted, ref, watch, watchEffect} from "vue";
//
import venice_sunset_1k from '../../assets/venice_sunset_1k.hdr?url'
import Car from '../../assets/car.glb?url'
import Plane from '../../assets/plane/plane.gltf?url'
import {RGBELoader} from "three/examples/jsm/loaders/RGBELoader";
import {DRACOLoader} from "three/examples/jsm/loaders/DRACOLoader";
import {GLTF, GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";

const scene = new Scene();

const grid = new GridHelper(20,40,0xfffff,0xffff);

const containerRef = ref<HTMLDivElement>()
const statsRef = ref<HTMLDivElement>()
//轨道控制器
const controlsRef = ref<OrbitControls>()
const bodyMaterial = new MeshPhysicalMaterial({
  color:0xff0000,metalness:1.0,roughness:0.5,clearcoat:1.0,clearcoatRoughness:0.03,sheen:0.5
})
const glassMaterial = new MeshPhysicalMaterial({
  color:0xffffff,metalness:0.25,roughness:0,transmission:1.0
})
const detailMaterial = new MeshPhysicalMaterial({
  color:0xff0000,metalness:1.0,roughness:0.5
})

const stats = new Stats();
const controlRef = ref({
  bodyColor:"#0c0c0c",
  glassColor:"#0c0c0c",
  detailColor:"#0c0c0c",
})
const cameraRef = ref<PerspectiveCamera>()
const rendererRef = ref<WebGLRenderer>()


watch(()=> controlRef.value.bodyColor,(c)=>{
  bodyMaterial.color.set(c);
})
watch(()=> controlRef.value.glassColor,(c)=>{
  glassMaterial.color.set(c);
})
watch(()=> controlRef.value.detailColor,(c)=>{
  detailMaterial.color.set(c);
})

//它会检查当前网页中是否存在具有特定类名（即".dg.ac>.dg.main.a"）的元素。
// 如果不存在，它将创建一个新的dat.GUI对象，
// 并在该对象中添加三个颜色控件：bodyColor、glassColor和detailColor。
// 这些颜色控件的值都是从名为controlRef的引用所指向的对象中获取的
function initGUI() {
  if(document.querySelectorAll(".dg.ac>.dg.main.a").length === 0){
    const gui = new dat.GUI()
    gui.addColor(controlRef.value,"bodyColor")
    gui.addColor(controlRef.value,"glassColor")
    gui.addColor(controlRef.value,"detailColor")
  }
}
/*
它使用PerspectiveCamera构造函数创建了一个新的透视相机对象。
这个构造函数需要四个参数：视角角度（在这里为45度）、纵横比（在这里为窗口宽度除以窗口高度）、近裁剪平面距离（在这里为0.1）
以及远裁剪平面距离（在这里为1000）。然后，它设置了摄像机的位置坐标为(-30,40,30)，使摄像机面向场景的位置
* */
function initCamera() {
  cameraRef.value =  new PerspectiveCamera(45,window.innerWidth / window.innerHeight,0.1,1000)
  cameraRef.value.position.set(-30,40,30)
  cameraRef.value.lookAt(scene.position)
}

/*

创建了一个新的WebGL渲染器并将其赋值给rendererRef。这个渲染器启用了抗锯齿功能，
设置了像素比例为窗口的设备像素比，
并将宽度和高度设置为窗口的内宽度和内高度。它还设置了输出编码为sRGBEncoding，
色调映射为ACESFilmicToneMapping，色调映射曝光为0.85。

* */
function initRenderer(){
  rendererRef.value = new WebGLRenderer({antialias:true})

  rendererRef.value.setPixelRatio(window.devicePixelRatio)
  rendererRef.value.setSize(window.innerWidth,window.innerHeight)
  rendererRef.value.outputEncoding = sRGBEncoding
  rendererRef.value.toneMapping = ACESFilmicToneMapping
  rendererRef.value.toneMappingExposure = 0.85
}

/*
将背景颜色设为深灰色（#333），然后加载名为"venice_sunset_1k"的环境贴图，并将其映射方式设为EquirectangularReflectionMapping。这将会使场景具有反射光照的效果。

接下来的几行代码修改了grid组件所用材质的透明度、深度写入以及透明度属性，使其呈现出半透明效果。

最后，它向场景中添加了一个长度为20的新坐标轴助手。
*
*/
function initScene(){
  scene.background = new Color(0x333333)
  scene.environment = new RGBELoader().load(venice_sunset_1k)
  scene.environment.mapping = EquirectangularReflectionMapping;
  //scene.fog = new Fog(0x333333,10,15)

  const material = grid.material as Material
  material.opacity = 0.2
  material.depthWrite = false
  material.transparent = true
  scene.add(grid)

  const axes = new AxesHelper(20)
  scene.add(axes)

}
const wheels: (Object3D | undefined)[] = []

function initGLTF() {
  const dracoLoader = new DRACOLoader();
  dracoLoader.setDecoderPath('/gltf/');
  const  loader = new GLTFLoader();
  loader.setDRACOLoader (dracoLoader) ;
  loader.load(Car,(gltf: GLTF) => {
    console.log(gltf)
    const carModel = gltf.scene.children[0];
    (carModel.getObjectByName('body') as Mesh).material = bodyMaterial;
    (carModel.getObjectByName('glass') as Mesh).material = glassMaterial;
    (carModel.getObjectByName('rim_fl') as Mesh).material = detailMaterial;
    (carModel.getObjectByName('rim_fr') as Mesh).material = detailMaterial;
    (carModel.getObjectByName('rim_rr') as Mesh).material = detailMaterial;
    (carModel.getObjectByName('rim_rl') as Mesh).material = detailMaterial;
    (carModel.getObjectByName('trim') as Mesh).material = detailMaterial;
    wheels.push(
        carModel.getObjectByName('wheel_fl'),
        carModel.getObjectByName('wheel_fr'),
        carModel.getObjectByName('wheel_rl'),
        carModel.getObjectByName('wheel_rr'),
    )
    scene.add(carModel)
  })


  const loaderPlane = new GLTFLoader();

  loaderPlane.load(
      Plane,
      // 加载完成后的回调函数
      function (gltf) {
        // 当模型加载完成后会调用该函数
        // 将模型添加到场景中
        scene.add(gltf.scene);
        gltf.scene.position.set(10,0,0)
        gltf.scene.rotation.y = 600
      },

      // 正在加载模型时的回调函数
      function (xhr) {
        // 进度条代码，可以在此编写代码，传入 xhr.loaded 和 xhr.total，计算出加载进度
      },
      // 加载出错的回调函数
      function (err) {
        console.error('An error happened.');
      }
  );
}

function initControl() {
  if(cameraRef.value) {
    controlsRef.value = new OrbitControls(cameraRef.value,containerRef.value);
    controlsRef.value.enableDamping = true
    controlsRef.value.maxDistance = 9
    controlsRef.value.target.set(0,0.5,0)
    controlsRef.value.update()
  }
}
initGUI()
initGLTF()
onMounted(()=>{
  //创建场景
  stats.dom.style.top = "50px"
  statsRef.value?.append(stats.dom)
  initScene()
  initCamera()
  initRenderer()
  initControl()
})

/*
持续运行的循环渲染函数，用于不断更新和重新绘制3D场景。

每一帧开始时，它会先调用stats对象的update方法，用于统计当前性能信息。

接着，它会调用requestAnimationFrame方法再次请求下一帧的动画。这个方法会在浏览器认为适合的时候安排一次重绘，通常是在下一次刷新周期之前。

如果存在cameraRef引用，则使用该相机进行当前帧的渲染。rendererRef指针表示的是已经初始化好的WebGL渲染器，而scene则是需要渲染的三维场景。

最后，根据性能计数器的时间戳，对网格物体的位置做了一次平移操作，使得网格能够以一定的速度沿Z轴方向移动。这里使用了取模运算符%来让网格的位置在其运动过程中始终保持在一个范围内。

注意requestAnimationFrame(renderScene)这一句的作用。这是一个JavaScript API，它可以在浏览器下次重绘之前，要求浏览器执行指定的函数（在这个例子中就是renderScene()）。这样做的好处是可以减少不必要的CPU和GPU工作，从而提高页面性能。

每个requestAnimationFrame()调用都会返回一个定时ID，你可以用这个ID取消未执行的动画。如果要停止动画，只需清除对应的定时ID即可。

在这个例子中，requestAnimationFrame(renderScene)会在每次渲染完成后立即调用自己，从而形成一个无限循环，不断地重复执行渲染过程。只要页面没有关闭，这个函数就会一直被调用下去。
* */
function renderScene() {
  stats.update()
  requestAnimationFrame(renderScene)
  if(cameraRef.value){
    rendererRef.value!.render(scene,cameraRef.value)
  }
  if(controlsRef.value){
    controlsRef.value.update()
  }
  const time = -performance.now() / 1000

  for (let i = 0;i < wheels.length;i++){
    wheels[i]!.rotation.x = time * Math.PI * 2
  }

  grid.position.z = -(time) % 1
}

renderScene()

/*
这段代码是一个Vue watchEffect钩子函数，当某些数据发生变化时，会触发此函数执行。

函数内部主要做了两件事：

将rendererRef的domElement属性（即渲染器的DOM元素）添加到containerRef指定的容器中。这意味着该渲染器将会在对应的HTML元素中显示。
在窗口大小发生改变时，监听并响应事件。当窗口尺寸发生改变时，会更新相机的宽高比，计算新的投影矩阵，并且重新设置渲染器的尺寸，使其与窗口尺寸保持一致。
因此，这段代码的作用是将渲染结果正确地显示出来，并确保在窗口尺寸改变时能够及时更新视口大小和视角。
* */
watchEffect(()=>{
  containerRef.value?.appendChild(rendererRef.value!.domElement)
  window.addEventListener('resize',()=>{
    cameraRef.value!.aspect = window.innerWidth / window.innerHeight
    //更新相投影矩阵
    cameraRef.value!.updateProjectionMatrix();
    rendererRef.value!.setSize(window.innerWidth,window.innerHeight)
  },false)
})

</script>

<style scoped>

</style>
